﻿using System.Net.Sockets;
using System.Net;
using System.Text;
using System;
using System.Threading;

namespace ACEMUD
{
    public class Program
    {
        
        static void Main(string[] args)
        {
            ACEMUD.MUD mud = new MUD();
            mud.RunMUD();
        }
    }

    public class MUD
    {
        public static ManualResetEvent allDone = new ManualResetEvent(false);

        public MultipleRooms Rooms = new MultipleRooms();
        public MultipleUsers Users = new MultipleUsers();

        /// <summary>
        /// Main method that boots up and initializes the MUD.
        /// </summary>
        public void RunMUD()
        {
            Console.WriteLine("ACEMUD");

            Console.WriteLine("Setting up the rooms...");

            Room tempRoom = new Room(0);
            tempRoom.Name = "An empty box.";
            tempRoom.Description = "You're sitting in a barren room.";

            Exit tempExit = new Exit(1);
            tempExit.Name = "East";
            tempExit.ShortName = "E";

            tempRoom.Exits.Add(tempExit);

            Rooms.Add(tempRoom);

            Room tempRoom1 = new Room(1);
            tempRoom1.Name = "An empty street.";
            tempRoom1.Description = "There is nothing here.  All you see is an barren looking building to the west.  The empty street goes on for miles and miles.";

            Exit tempExit1 = new Exit(0);
            tempExit1.Name = "West";
            tempExit1.ShortName = "W";
            tempRoom1.Exits.Add(tempExit1);

            Exit tempExit2 = new Exit(2);
            tempExit2.Name = "Up";
            tempExit2.ShortName = "u";
            tempRoom1.Exits.Add(tempExit2);

            Rooms.Add(tempRoom1);


            Console.WriteLine("\nListening on port: 4004");
            Console.WriteLine("Max Users: 20");

            new Thread(WaitForConnection).Start();
        }

        /// <summary>
        /// Method to wait for a connection.
        /// </summary>
        void WaitForConnection()
        {
            Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            server.Bind(new IPEndPoint(IPAddress.Any, 4004));   // Port

            server.Listen(20);  // Max connections

            while (true)
            {
                allDone.Reset();
                server.BeginAccept(new AsyncCallback(AcceptCallback), server);
                allDone.WaitOne();
            }            
        }

        public void AcceptCallback(IAsyncResult ar)
        {
            // Signal the main thread to continue.
            allDone.Set();

            // Get the socket that handles the client request.
            Socket listener = (Socket)ar.AsyncState;
            Socket handler = listener.EndAccept(ar);

            User user = new User(handler);
            
            Users.Add(user);
            
            Send(user, "Welcome to ACEMUD!\n");
            Send(user, "What is your name, young adventurer: ", false);
            handler.BeginReceive(user.buffer, 0, 1024, 0, new AsyncCallback(ReadCallback), user);
        }

        public void ReadCallback(IAsyncResult ar)
        {
            String content = String.Empty;

            // Retrieve the state object and the handler socket
            // from the asynchronous state object.
            User user = (User)ar.AsyncState;
            Socket handler = user.Socket;

            // Read data from the client socket. 
            int bytesRead = handler.EndReceive(ar);

            if (bytesRead > 0)
            {
                // There  might be more data, so store the data received so far.
                user.sb.Append(Encoding.ASCII.GetString(user.buffer, 0, bytesRead));

                // Check for end-of-file tag. If it is not there, read more data.
                content = user.sb.ToString();
                if (content.IndexOf("\n") > -1)
                {
                    content = content.Trim();

                    if (user.Name.Length == 0)
                    {
                        user.Name = content;
                        user.Room = 0;
                        Send(user, "\n\n");
                        SayAllRoom(Ansi.StyleBold + Ansi.ForeMagenta + "* " + Ansi.ForeYellow + user.Name + Ansi.ForeGreen + " has appeared out of the void!", user);
                        ParseCommand("look", user);
                    }
                    else
                    {
                        ParseCommand(content, user);
                    }
                    user.sb.Clear();
                    handler.BeginReceive(user.buffer, 0, 1024, 0, new AsyncCallback(ReadCallback), user);
                }
                else
                {
                    // Not all data received. Get more.
                    handler.BeginReceive(user.buffer, 0, 1024, 0, new AsyncCallback(ReadCallback), user);
                }
            }
        }

        /// <summary>
        /// Sends a line of text to a user.
        /// </summary>
        /// <param name="user">A User object which the text is to be sent to.</param>
        /// <param name="data">A string containing the text to send.</param>
        /// <param name="newLine">A boolean specifying whether a new line should be appended to the end or not.  Default is yes.</param>
        private static void Send(User user, String data, bool newLine = true)    
        {
            data += Ansi.StyleReset;

            if (newLine)
                data += "\r\n";

            // Convert the string data to byte data using ASCII encoding.
            byte[] byteData = Encoding.ASCII.GetBytes(data);

            // Begin sending the data to the remote device.
            try
            {
                user.Socket.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), user.Socket);
            }
            catch // (Exception e)
            {
                // TODO Check that the user socket is disconnected; if so, save the user and delete the users' object.
            }
        }

        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket handler = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.
                int bytesSent = handler.EndSend(ar);
                //handler.Shutdown(SocketShutdown.Both);
                //handler.Close();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        /// <summary>
        /// Sends a line of text to all connected clients.
        /// </summary>
        /// <param name="text">A string containing the text to send.</param>
        /// <param name="fromSystem">A boolean signifying if it's a system message or not.  If true, ALL users connected will receive the message.  If false, only users logged in will receive the message.</param>
        void SayAll(string text, bool fromSystem = false)
        {
            text += Ansi.StyleReset;
            foreach (User user in Users)
            {
                if (fromSystem)
                {
                    Send(user, text);
                }
                else
                {
                    if (user.Name.Length != 0)
                        Send(user, text);
                }
            }
        }

        /// <summary>
        /// Sends a line of text to all users in the room (minus originating user).
        /// </summary>
        /// <param name="text">A string containing the text to send.</param>
        /// <param name="originatingUser">A user object containing the user to exclude from this.</param>
        void SayAllRoom(string text, User originatingUser)
        {
            text += Ansi.StyleReset;
            foreach (User user in Users)
            {
                if (user.Room == originatingUser.Room)
                    if (originatingUser.Name.ToLower() != user.Name.ToLower())
                        if (user.Name.Length != 0)
                            Send(user, text);
            }
        }
        
        /// <summary>
        /// Sends a line of text to all connected clients (minus the originating user).
        /// </summary>
        /// <param name="text">A string containing the text to send.</param>
        /// <param name="originatingUser">A User object containing the user to exclude from this.</param>
        void SayAll(string text, User originatingUser)
        {
            text += Ansi.StyleReset;
            foreach (User user in Users)
            {
                if (originatingUser.Name.ToLower() != user.Name.ToLower())
                    if (user.Name.Length != 0)
                        Send(user, text);
            }
        }
        
        /// <summary>
        /// Method to parse a command that was typed for the user and take action on it.
        /// </summary>
        /// <param name="command">A string containing the full command the user typed.</param>
        /// <param name="user">A User object which contains the user who typed the command.</param>
        private void ParseCommand(string command, User user)
        {
            command = command.Trim();

            // Get all the words from the text that was entered so it can be parsed
            string[] words = command.Split(' ');
            string verb = words[0].ToLower();

            Room tempRoom = FindRoomByID(user.Room);

            switch (verb)
            {
                case "'":
                case "say":
                    command = command.Remove(0, verb.Length + 1).Trim();
                    Send(user, Ansi.StyleBold + Ansi.ForeYellow + "You" + Ansi.ForeGreen + " say '" + command + "'");
                    SayAll(Ansi.StyleBold + Ansi.ForeYellow + user.Name + Ansi.ForeGreen + " says '" + command + "'", user);
                    break;
                case "look":
                    //Send(user, "Looking for room: " + user.Room.ToString());
                    if (tempRoom != null)
                    {
                        Send(user, Ansi.ForeWhite + tempRoom.Name);
                        Send(user, tempRoom.Description);
                        Send(user, Ansi.StyleBold + Ansi.ForeYellow + "\nExits:");
                        
                        // Show all the exits
                        if (tempRoom.Exits.Count > 0)
                        {
                            foreach (Exit tempExit in tempRoom.Exits)
                            {
                                Send(user, tempExit.Name);
                            }
                        }
                        else
                        {
                            Send(user, "No noticable exits.");
                        }

                        // Show all the users in the room
                        foreach (User tempUser in Users)
                        {
                            if (tempUser.Room == user.Room)
                            {
                                if (user.Name.ToLower() != tempUser.Name.ToLower())
                                    Send(user, Ansi.StyleBold + Ansi.ForeGreen + tempUser.Name + " is here.");
                            }
                        }
                        Send(user, Ansi.StyleBold + Ansi.ForeGreen + "You are here.");
                    }
                    else
                        Send(user, Ansi.ForeRed + "ROOM NOT FOUND!");
                    break;
                case "who":
                    Send(user, "Name:\t\t\tConnected State:");
                    foreach (User tempUser in Users)
                    {
                        if (tempUser.Name.Length == 0)
                            Send(user, "(UNKNOWN USER)\t\t" + tempUser.Socket.Connected.ToString());
                        else
                            Send(user, tempUser.Name + "\t\t\t" + tempUser.Socket.Connected.ToString());
                    }
                    break;
                case "/me":
                case "emote":
                    command = command.Remove(0, verb.Length + 1).Trim();
                    Send(user, Ansi.StyleBold + Ansi.ForeYellow + "You " + Ansi.ForeMagenta + command);
                    SayAll(Ansi.StyleBold + Ansi.ForeYellow + user.Name + Ansi.ForeMagenta + " " + command, user);
                    break;
                default:
                    // Since none of the default commands worked, check through the dynamic content
                    bool didCommand = false;

                    // Loop through all the exits to see if we can move through it.
                    try
                    {
                        if (tempRoom.Exits.Count > 0)
                        {
                            foreach (Exit tempExit in tempRoom.Exits)
                            {
                                if ((verb == tempExit.Name.ToLower()) || (verb == tempExit.ShortName.ToLower()))
                                {
                                    // The verb = exit command, so see if the user is able to move to that room
                                    if (tempExit.State)
                                    {
                                        // Can move that way!
                                        user.Room = tempExit.LinkToRoom;
                                        ParseCommand("look", user);
                                    }
                                    else
                                    {
                                        // Way is blocked
                                        Send(user, "Your way is blocked.");
                                    }
                                    didCommand = true;
                                }
                            }
                        }
                    }
                    catch
                    {
                        // TODO Add error catching here.
                    }

                    if (!didCommand)
                        Send(user, Ansi.StyleBold + Ansi.ForeGreen + "That doesn't make sense!");
                    break;
            }
            Send(user, "\n>", false);
        }
        
        /// <summary>
        /// Returns a Room object based on the supplied IDNum for the room.
        /// </summary>
        /// <param name="IDNum">A uint that contains the room ID being searched for.</param>
        /// <returns>A Room object that was found.  If it wasn't found, returns null.</returns>
        private Room FindRoomByID(uint IDNum)
        {
            foreach (Room tempRoom in Rooms)
            {
                if (tempRoom.RoomID == IDNum)
                    return tempRoom;
            }

            // No room was found, so return null.
            return null;
        }
    }

    public static class Ansi
    {
        public static string StyleReset = "\x1B[0m";
        public static string StyleBold = "\x1B[1m";
        public static string StyleClear = "\x1B[2J";
        public static string ForeBlack = "\x1B[30m";
        public static string ForeRed = "\x1B[31m";
        public static string ForeGreen = "\x1B[32m";
        public static string ForeYellow = "\x1B[33m";
        public static string ForeBlue = "\x1B[34m";
        public static string ForeMagenta = "\x1B[35m";
        public static string ForeCyan = "\x1B[36m";
        public static string ForeWhite = "\x1B[37m";
        public static string BackBlack = "\x1B[40m";
        public static string BackRed = "\x1B[41m";
        public static string BackGreen = "\x1B[42m";
        public static string BackYellow = "\x1B[43m";
        public static string BackBlue = "\x1B[44m";
        public static string BackMagenta = "\x1B[45m";
        public static string BackCyan = "\x1B[46m";
        public static string BackWhite = "\x1B[47m";
    }
}